#!/usr/bin/env python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

import os
import re
import copy
import tempfile
import contextlib
import strict_hwcfg
from .._util import Util
from .._game_straits import GameStraitForDos, GameStraitNeedJoysticks
from .._runner import RunnerBase


class DosBox(RunnerBase):

    def __init__(self, verbose=0):
        super().__init__()
        self._verbose = verbose

    def _initialize(self, sys_info, game_id, game_obj):
        super()._initialize(sys_info, game_id, game_obj)

        assert game_obj.has_strait(GameStraitForDos)

        # check game_object.spec
        if hasattr(game_obj.spec, "languages"):
            # FIXME
            pass
        if hasattr(game_obj.spec, "default_language"):
            # FIXME
            pass
        if hasattr(game_obj.spec, "resolutions"):
            # FIXME
            pass

        self._gameObj = game_obj

    def _prepare(self, context):
        self._context = context
        self._batchCall("_prepare", self._context)

        if self._gameObj.has_strait(GameStraitNeedJoysticks):
            self._context.joysticks = Util.hwcfgGeJoystickComponent(strict_hwcfg.probe())

    def _processGameHint(self, hint):
        self._hint = hint

    async def _run(self, item):
        self.__applyQuirkForGameObj()

        # make sure all the modifications are saved
        self._gameObj._commit()

        runInfo = self._gameObj._getRunInfo(item)
        with self.__createDosBoxMapperFile(runInfo) as mapperFn:
            with self.__createDosBoxConfFile(mapperFn, runInfo) as fname:
                cmdList = ["dosbox", "--noprimaryconf", "--nolocalconf", "--conf", fname, "--exit"]

                if runInfo.extra_environemnt_variables is not None:
                    envDict = copy.copy(os.environ)
                    envDict.update(runInfo.extra_environemnt_variables)
                else:
                    envDict = os.environ

                await Util.subprocess_exec_check_call(*cmdList, env=envDict)

    def _cleanup(self, on_error=False):
        if hasattr(self, "_context"):
            self._batchCall("_cleanup", self._context, on_error=on_error)
            del self._context

    @contextlib.contextmanager
    def __createDosBoxMapperFile(self, runInfo):
        # apply quirks
        bNeedKeyMapping = self.__applyQuirkForNeedKepMapping(runInfo)

        if bNeedKeyMapping:
            buf = ""

            dstMapper = self.__parseDefaultMapper()
            for k, v in dstMapper.items():
                buf += "%s %s\n" % (k, " ".join(["\"%s\"" % (x) for x in v]))

            # apply quirks
            buf = self.__applyQuirkForDosBoxMapperFile(runInfo, buf)

            with tempfile.NamedTemporaryFile("w", delete_on_close=False) as f:
                f.write(buf)
                f.close()
                yield f.name
        else:
            yield None

    @contextlib.contextmanager
    def __createDosBoxConfFile(self, mapperFn, runInfo):
        # the game has non-standard resolution (eg 300x200)
        # aspect = true
        buf = ""

        buf += "[joystick]\n"
        buf += "joysticktype = disabled\n"
        buf += "\n"

        buf += "[sdl]\n"
        if mapperFn is not None:
            buf += "mapperfile = %s" % (mapperFn)
        buf += "\n"

        buf += "[autoexec]\n"
        buf += "@echo off\n"
        buf += "MOUNT C %s\n" % (runInfo.drive_c_dirpath)
        if runInfo.cdrom_filepath is not None:
            buf += "IMGMOUNT D %s -t iso -fs iso\n" % (runInfo.cdrom_filepath)
        buf += "C:\n"
        buf += "CLS\n"
        if True:
            executable = runInfo.command.split(" ")[0]
            if executable.endswith(".BAT") or executable.endswith(".bat"):
                # bat file must be preprended with "call", or it won't terminate after running
                buf += "CALL " + runInfo.command + "\n"
            else:
                buf += runInfo.command + "\n"

        # apply quirks
        buf = self.__applyQuirkForDosBoxConfFile(runInfo, buf)

        # do work
        with tempfile.NamedTemporaryFile("w", delete_on_close=False) as f:
            f.write(buf)
            f.close()
            yield f.name

    def __applyQuirkForGameObj(self):
        self._quirkClass = self.__findClass("Quirk" + self._gameObj.__class__.__name__)        # self._quirkClass is also used by other self.__applyQuirkForXXX() series functions
        if self._quirkClass is not None and hasattr(self._quirkClass, "forGameObj"):
            self._quirkClass.forGameObj(self._context, self._hint, self._gameObj)

    def __applyQuirkForNeedKepMapping(self, runInfo):
        if self._quirkClass is not None and hasattr(self._quirkClass, "forNeedKeyMapping"):
            return self._quirkClass.forNeedKeyMapping(self._context, self._hint, runInfo)
        else:
            return False

    def __applyQuirkForDosBoxMapperFile(self, runInfo, buf):
        if self._quirkClass is not None and hasattr(self._quirkClass, "forDosBoxMapperFile"):
            return self._quirkClass.forDosBoxMapperFile(self._context, self._hint, runInfo, buf)
        else:
            return buf

    def __applyQuirkForDosBoxConfFile(self, runInfo, buf):
        if self._quirkClass is not None and hasattr(self._quirkClass, "forDosBoxConfFile"):
            return self._quirkClass.forDosBoxConfFile(self._context, self._hint, runInfo, buf)
        else:
            return buf

    @staticmethod
    def __findClass(name):
        for obj_name, obj in globals().items():
            if not isinstance(obj, type):
                continue
            if obj_name != name:
                continue
            return obj
        return None

    # FIXME: dosbox sucks that there's no way to specify only custom mapping
    @staticmethod
    def __parseDefaultMapper():
        defaultMapper = """
key_esc "key 41"
key_f1 "key 58"
key_f2 "key 59"
key_f3 "key 60"
key_f4 "key 61"
key_f5 "key 62"
key_f6 "key 63"
key_f7 "key 64"
key_f8 "key 65"
key_f9 "key 66"
key_f10 "key 67"
key_f11 "key 68"
key_f12 "key 69"
key_grave "key 53"
key_1 "key 30"
key_2 "key 31"
key_3 "key 32"
key_4 "key 33"
key_5 "key 34"
key_6 "key 35"
key_7 "key 36"
key_8 "key 37"
key_9 "key 38"
key_0 "key 39"
key_minus "key 45"
key_equals "key 46"
key_bspace "key 42"
key_tab "key 43"
key_q "key 20"
key_w "key 26"
key_e "key 8"
key_r "key 21"
key_t "key 23"
key_y "key 28"
key_u "key 24"
key_i "key 12"
key_o "key 18"
key_p "key 19"
key_lbracket "key 47"
key_rbracket "key 48"
key_enter "key 40"
key_capslock "key 57"
key_a "key 4"
key_s "key 22"
key_d "key 7"
key_f "key 9"
key_g "key 10"
key_h "key 11"
key_j "key 13"
key_k "key 14"
key_l "key 15"
key_semicolon "key 51"
key_quote "key 52"
key_backslash "key 49"
key_lshift "key 225"
key_oem102 "key 100"
key_z "key 29"
key_x "key 27"
key_c "key 6"
key_v "key 25"
key_b "key 5"
key_n "key 17"
key_m "key 16"
key_comma "key 54"
key_period "key 55"
key_slash "key 56"
key_abnt1 "key 135"
key_rshift "key 229"
key_lctrl "key 224"
key_lgui "key 227"
key_lalt "key 226"
key_space "key 44"
key_ralt "key 230"
key_rgui "key 231"
key_rctrl "key 228"
key_printscreen "key 70"
key_scrolllock "key 71"
key_pause "key 72"
key_insert "key 73"
key_home "key 74"
key_pageup "key 75"
key_delete "key 76"
key_end "key 77"
key_pagedown "key 78"
key_up "key 82"
key_left "key 80"
key_down "key 81"
key_right "key 79"
key_numlock "key 83"
key_kp_divide "key 84"
key_kp_multiply "key 85"
key_kp_minus "key 86"
key_kp_7 "key 95"
key_kp_8 "key 96"
key_kp_9 "key 97"
key_kp_plus "key 87"
key_kp_4 "key 92"
key_kp_5 "key 93"
key_kp_6 "key 94"
key_kp_1 "key 89"
key_kp_2 "key 90"
key_kp_3 "key 91"
key_kp_enter "key 88"
key_kp_0 "key 98"
key_kp_period "key 99"

jbutton_0_0 "stick_0 button 0"
jbutton_0_1 "stick_0 button 1"
jaxis_0_1- "stick_0 axis 1 0"
jaxis_0_1+ "stick_0 axis 1 1"
jaxis_0_0- "stick_0 axis 0 0"
jaxis_0_0+ "stick_0 axis 0 1"
jbutton_0_2 "stick_0 button 2"
jbutton_0_3 "stick_0 button 3"
jbutton_1_0
jbutton_1_1
jaxis_0_2- "stick_0 axis 2 1"
jaxis_0_2+ "stick_0 axis 5 1"
jaxis_0_3- "stick_0 axis 4 0"
jaxis_0_3+ "stick_0 axis 4 1"
jaxis_1_0-
jaxis_1_0+
jaxis_1_1-
jaxis_1_1+
jbutton_0_4 "stick_0 button 4"
jbutton_0_5 "stick_0 button 5"
jhat_0_0_0 "stick_0 hat 0 1"
jhat_0_0_3 "stick_0 hat 0 8"
jhat_0_0_2 "stick_0 hat 0 4"
jhat_0_0_1 "stick_0 hat 0 2"
mod_1 "key 224" "key 228"
mod_2 "key 226" "key 230"
mod_3 "key 227" "key 231"
"""

        ret = {}
        import re
        for line in defaultMapper.split("\n"):
            if line == "":
                continue
            if " " not in line:
                ret[line] = []
            else:
                m = re.fullmatch(r'(\S+) (.+)', line)
                ret[m.group(1)] = eval("[%s]" % (m.group(2).replace("\" \"", "\",\"")))
        return ret


class QuirkSimCity2000:

    @staticmethod
    def forGameObj(context, hint, gameObj):
        gameObj.apply_sound_hardware_type("SoundBlaster16")

    @staticmethod
    def forDosBoxConfFile(context, hint, runInfo, buf):
        # see https://github.com/dosbox-staging/dosbox-staging/wiki/Game-issues#simcity-2000
        return buf.replace("CLS\n", "LOADFIX\nCLS\n")


class QuirkF22Lightning2:

    @staticmethod
    def forNeedKeyMapping(context, hint, runInfo):
        if len(hint.selected_joystick_indices) > 0:
            c = context.joysticks[hint.selected_joystick_indices[0]]
            if c.get_vendor() == "microsoft" and c.get_model() == "xone-controller":
                return True
        return False

    @staticmethod
    def forDosBoxMapperFile(context, hint, runInfo, buf):
        if len(hint.selected_joystick_indices) > 0:
            c = context.joysticks[hint.selected_joystick_indices[0]]
            if c.get_vendor() == "microsoft" and c.get_model() == "xone-controller":
                # maps LT (axis 2 1) and RT (axis 5 1) as rudder
                buf = re.sub(r'jaxis_0_2- .*', 'jaxis_0_2- "stick_0 axis 2 1"', buf, flags=re.M)
                buf = re.sub(r'jaxis_0_2\+ .*', 'jaxis_0_2+ "stick_0 axis 5 1"', buf, flags=re.M)
        return buf

    @staticmethod
    def forDosBoxConfFile(context, hint, runInfo, buf):
        if len(hint.selected_joystick_indices) > 0:
            # convert all kinds of gamepad, flight-stick to ch joystick
            return buf.replace("joysticktype = disabled\n", "joysticktype = ch\n")
        else:
            return buf
